%\documentclass[a4paper,20pt]{report}
\documentclass[a4paper,usenames,dvipsnames,12pt]{report}
\usepackage{amsmath}
\usepackage[french]{babel}
%\usepackage[latin1]{inputenc}
\usepackage{graphicx}
\usepackage[utf8]{inputenc}
\usepackage{setspace}
\usepackage{fancyhdr}
\usepackage{multicol}
\usepackage{verbatim}
\usepackage{amsfonts}
\usepackage{color}
\usepackage{listings}
%\usepackage{mathtools}
\usepackage{amssymb}
\usepackage{comment}
%\usepackage{vmargin}
\usepackage{geometry}
\usepackage{pifont}
\usepackage[urlcolor=blue]{hyperref}
\usepackage{graphics}
\usepackage{lastpage}
%\usepackage{float}
%\usepackage[T1]{fontenc}
\usepackage{geometry}


%\usepackage{subfigure}

%%%%%%%%%%%%%%%% Lengths %%%%%%%%%%%%%%%%
%\setlength{\textwidth}{20cm}
%\setlength{\evensidemargin}{1cm}
%\setlength{\oddsidemargin}{1cm}
\geometry{top=3cm, bottom=3cm}
%%%%%%%%%%%%% Java Listings %%%%%%%%%%%%%
\definecolor{jbackground}{rgb}{0.85,0.85,0.85}
\lstset{language=C, backgroundcolor=\color{jbackground}}


%\title{\huge{Rapport de Systeme de Gestion de Base de Donnees}}
%\author{HAILASS Abdessamad et OUIDANI Youssef\\$2^{eme}$ annee ENSEIRB-MATMECA Informatique}
%\date {\today}

%\geometry{a4paper,twoside,left=3cm,right=3cm,marginparwidth=1.2cm,%
%marginparsep=3mm,top=3cm,bottom=4cm}




%%%%%%%%%%%%%%%% Variables %%%%%%%%%%%%%%%%
\def\titre{Projet S8:\\Compte-Rendu :\\Application d’échange de Fichiers en Pair à Pair (FileShare)}
\def\titregauche{Projet S8}
\def\titredroite{Compte-rendu de projet système et réseau} 
\def\filiere{informatique}
\def\annee{$2^{eme}$}  % $1^{ere}$, $2^{eme}$ ou $3^{eme}$
\def\anneescolaire{2011/2012}
\def\promotion{2013}
\def\equipe{SRIBTI Otman\\OUIDANI Youssef\\AIGRON Romain\\SLIMANI Houti Ali}
\def\encadrant{Antoine ROLLET} 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


\pagestyle{fancy} \lhead{\textsc{\titregauche}}
\rfoot{\textit{Année \anneescolaire}}\rhead{\textsc{\titredroite}}\cfoot{\thepage/\pageref{LastPage}}
\renewcommand{\headrulewidth}{0.4pt}
\renewcommand{\footrulewidth}{0.4pt}

\newcommand{\HRule}{\rule{\linewidth}{0.5mm}}





\begin{document}

\begin{titlepage}

\begin{center}


% Upper part of the page
\includegraphics[width=0.3\textwidth]{logo}\\[1cm]

\textsc{\LARGE ENSEIRB-MATMECA}\\[1cm]

\textsc{\Large {Filière \filiere, \annee année}}\\[0.5cm]
%\textsc{\Large {Promotion \promotion}}\\[0.5cm]

% Title
\HRule \\[0.4cm]
{ \huge \bfseries \titre}\\[0.4cm]

\HRule \\[1.5cm]

% Author and supervisor
\begin{minipage}{0.4\textwidth}
\begin{flushleft} \large
\emph{Auteurs :}\\
\equipe
\end{flushleft}
\end{minipage}
\begin{minipage}{0.4\textwidth}
\begin{flushright} \large
\emph{Encadrant :} \\
\encadrant
\end{flushright}
\end{minipage}

\vfill

% Bottom of the page
{\large \today}

\end{center}

\end{titlepage}



%\tableofcontents\thispagestyle{fancy}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


%%partie principale%%
\section*{Introduction}\thispagestyle{fancy}

Ce document est compte-rendu de notre travail sur le projet de Réseaux et Systèmes du semestre 8 à l’ENSEIRB.
Il traite de la réalisation une application pour le partage de fichiers en mode pair à pair. 
Un réseau pair-à-pair (ou P2P : Peer-to-Peer) à l'instar de \textbf{BitTorrent}.
Dans ce rapport, nous décrirons le travail réalisé, l'architecture globale de la réalisation ainsi que les différentes problématiques
que nous avons rencontrées et, pour certaines, les solutions que nous avons adoptées.

\section*{Réalisation}
La réalisation de l'application devant se faire en langages \texttt{C} ET \texttt{JAVA}, nous avons opté pour coder le Tracker en \texttt{JAVA}
et le client en \texttt{C} 
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\section*{Tracker}
Dans le cadre de la version centralisée, le tracker est l'entité centrale. Celui-ci est connu de tous les pairs 
à travers leurs identifications. Il regroupe toutes les informations concernant les différents pairs et ainsi permet 
d'assister les pairs dans leur recherche de fichiers.  

\subsection*{Fonctionalités}
La première fonctionnalité du tracker est de récupérer les informations d'un pair. Pour ce faire, le pair lui envoie le message \verb+announce+. 
À chaque réception de ce message il parse les différentes données reçues, les insère dans la base de données et enfin envoie un message 
de confirmation au pair. 
Le tracker doit pouvoir également transmettre les noms de l'ensemble des fichiers répondant à un certains nombre de critères à travers 
la requête \verb+look+.
Le tracker doit renvoyer l'ensemble des pairs possédant une partie ou la totalité d'un fichier à partir de sa clé.

Pour pouvoir gérer ces fonctionnalités, le tracker écoute sur l'un de ses port en attente d'une connexion. 
À la suite de la connexion celui-ci reçoit un message. Pour chacun de ces messages, un thread est crée 
réalisant les opérations décrites précédemment afin d'éviter le blocage du port d'écoute. 
Il est laissé le choix au client de couper ou non la connection après chaque commande.

\subsection*{Implémentation}

Le traitement des requêtes du client nécessite de maintenir une structure de données recensant les pairs et les fichiers qu'ils partagent. 
Cette structure doit aussi se prêter à la recherche par critères. Nous avions tout d'abord songé à une structure proche d'une table de hachage. 
Mais il est vite apparu qu'une base de données était parfaitement adéquate. Nous avons donc mis en place une base de données 
\texttt{hsqldb} en mémoire. Nous la manipulons par l'intermédiaire du framework \textbf{Hibernate} qui permet de réaliser un mapping objet-relationnel.

\subsection*{Description de l'architecture}

Le tracker écoute en continu sur l'un de ses ports. À chaque connexion établie, un thread est créé pour prendre en charge le message reçu. 
Ce thread parse la requête envoyées par le pair. Si celle-ci n'est pas validé elle ne sera pas prise en compte. Une fois validé, 
la commande associé à la requête est construite puis exécutée. L'exécution nécessite un dialogue avec la base de données. 
Une fois exécuté, un message est construit pour être renvoyé au pair.\\
Afin de maintenir à jour la base de données, chaque entrée sera munie d'un timestamp. Dans un premier temps, nous pensons 
déléguer la suppression des entités obsolètes à un thread à part mais cela peut nécessiter une configuration 
plus fine de la base pour que les accès concurrents ne soient pas trop pénalisant vis-à-vis des performances.\\
On peut voir sur la figure tracker \ref{Tracker} le schéma récapitulatif du fonctionnement du tracker.



\subsection*{L'interprétation des requêtes}
Pour simplifier le parsage des requêtes, nous avons étudiés les solutions
dédiées en Java. Nous nous sommes tout d'abords dirigés vers  
l'utilisation d'un
lexer (JFlex) et d'un analyseur syntaxique se basant sur une grammaire (Cup),
qui sont des solutions couramment utilisées. Malheureusement, Cup est en fait
un analyseur LRAR, c'est-à-dire qu'il regarde un token à l'avance. Ceci n'est
absolument pas compatible avec le contexte réseau dans lequel on se place,où
le dernier caractère n'est pas défini.

La solution finalement retenue est l'utilisation du lexer seul. Celui-ci offre
en effet tout les éléments (regex, états) pour créer un automate  
capable de reconnaître
et agir en fonction des requêtes au fil de leurs arrivées.
Cette solution permet de garder une relative souplesse vis-à-vis du  
protocole et
 de la gestion des erreurs tout en garantissant de bonnes performances.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\section*{Peer/Client}
\subsection*{Etablissement de la connexion}
Il s'agit de l'axe central du projet, on souhaite créer un programme capable d'établir un flot de données en délivrance garantie c-à-d initier 
une connexion avec un \texttt{host} (Tracker/Peer), d'être en écoute 
des envois de requêtes d'autres pairs ou réponse du tracker, et de transmettre des requêtes ainsi que des données le cas échant en utilisant 
l'API Socket en flots TCP.
Ceci impose d'avoir des rôles dits dissymétriques, et donc une architecture client serveur.
Dans un premier élan, nous avons dissocié ces 2 fonctions en créant 2 entités distinctes chacune responsable d'assumer un rôle. Mais, 
au fur et à mesure de
notre avancement, il est apparu clairement qu'un pair \ref{Pair} se devait d'assurer les 2 rôles, ce qui nous a poussé à fusionner les entités en une.
Nous présentons chacune des parties du pair \ref{Pair} :

\textbf{Partie client}\\
Elle se charge de :
\begin{itemize}
\item Création d'une socket internet de type stream : Connexion à une adresse IP et un numéro de port choisis
\item Dialogue bidirectionnel avec le serveur contacté
\end{itemize}
Pour cela, il faut 
\begin{itemize}
\item Instancier une socket client
\item Récupérer l'adresse de l'hôte avec la fonction \texttt{gethostbyname()}
\item Etablier la connexion avec le serveur via \texttt{connect()}
\item Envoyer la requête au serveur
\item Attente de la réponse de la transaction
\item Interruption de la connexion (et fermeture de la socket le cas échéant) 
\end{itemize}

\textbf{Partie serveur}\\
Son rôle essentiel est l'acceptation de connexion clients et initier un dialogue bidirectionnel avec chacune de ces connexions :
\begin{itemize}
\item Création d'une socket internet de type stream 
\item Affecter une adresse locale à la socket avec \texttt{Bind()}
\item Mettre la socket en mode passive pour l'acceptation des requêtes client
\item Accepter une connexion client et créer une nouvelle socket $nwsocket$
qui sera dédiée au service du client avec la fonction \texttt{accept()}
\item Traitement de la requête du client
\end{itemize}

%Cette méthode marche très bien mais à l'inconvénient d'être restrictive : 
%\begin{itemize}
%\item les clients ne peuvent être servis que l'un après l'autre, 
%\item Très pénalisant en cas de long dialogue (transfert volumineux...)
%\item Très dépendante du bon comportement des clients
%\item Le mode est connecté : prévoir le cas d'un client qui se connecte mais ne dialogue pas
%\item Prévoir le cas d'un client ne se déconnecte pas
%\end{itemize}

Pour pallier à un certain nombre de ces problème, une manière de faire est d'opter pour un serveur TCP multi-threadés.

\subsection*{Serveur multi-threadé}
L'idée principale est d'affecter chaque traitement bloquant à un thread distinct ce qui libère le thread principal.
Ainsi le programme principal ne sera chargé que d'accepter les nouvelles connexions et tout dialogue
est légué à un nouveau thread.
Finalement, nous nous retrouvons avec une architecture n-tiers où il y a autant de traitements en parallèle que de clients
l'avantage étant que chacun peut rester bloqué sans bloquer l'ensemble, en plus de faciliter l'écriture des dialogues avec les clients.
\\
Dans le cas de la commande \texttt{getpieces}; pour récupérer les morceaux de fichier, le serveur devra ouvrir une nouvelle connexion
sur un nouveau port. Ainsi, lorsque le client fera la demande d'une partition d'un fichier, le serveur enverra un numéro de port
sur lequel se connectera le client pour entamer par la suite le transfert.\\
Cette solution satisfait bien le cahier des charges néanmoins il est nécessaire d'analyser les inconvients qui découlent de ce choix 
d'implémentation. En effet, plusieurs inconvients surgissent quant à ce choix dont :
\begin{itemize}
\item Très peu performant en cas de forte charge : au bout d'un certain nombre de pairs, l'allocation de plusieurs piles d'éxecution et entrées
de l'ordannanceur affaiblit le système.
\item Le temps calculé pour la gestion du thread dépasse sensiblement le temps de traitement des requêtes (surtout celles qui demandent un dialogue 
court)
\item certains des élements de notre application sont communs à tous les threads notamment la base de données, ce qui stipule l'usage de mécanisme
de d'isolement de section critique (par exemple: le flags qui indique aux threads que le pair est en train d'envoyer un fichier Cf Algorithme de Téléchargement) 
\end{itemize}


\subsection*{Module de génération des clefs d'identification}
Ce module est responsable de la génération des clefs indispensable à l'identification du contenu d'un fichier et qui représente la base 
du protocole de communication
de notre application. Notre choix s'est porté sur l'algorithme \texttt{MD5}, une fonction de hachage cryptographique permettant d'obtenir 
l'empreinte numérique d'un fichier inventé par \textit{Ronald Rivest}\footnote{réalisé en 1991}. Pour la réalisation de ce module, 
nous nous sommes reposés sur un code existant de même auteur
auquel on a rajouté quelques fonctions pour compléter et qui simplifient le traitement par la suite par exemple :

//Fonction qui prend en paramètre un nom de fichier et génère sa clef MD5 puis la retransrire par effet de bord sur la chaîne \texttt{clef}
\begin{lstlisting}
int md5_fichier(char * nom_fichier, char * clef)
\end{lstlisting}  

\subsection*{Les Bases de données}
Pour représenter la base de données qui hébergera les fichiers, nous avons défini une structure \texttt{TableEntrée}
qui est implémentée sur la base d'un vecteur de type \texttt{Entrée}. chaque entrée représentant une instance de fichier de
la base de donnée.\\
Chaque entrée regroupe conformément aux données du problème les champs suivants : 
\begin{itemize}
\item le nom du fichier
\item La taille (en octets)
\item La clef d'identificaton MD5
\item l'adresse \texttt{IP} du propriétaire
\item Le numéro du port d'accès
\end{itemize}

\begin{lstlisting}
typedef struct TableFichiers{
  EntreeTable contenu[NB_FICHIERS]; // Contenu de la table
  int fin_table;   // Indice de fin de la table
}TableFichiers;
\end{lstlisting}
Chaque table comprend un certain nombre d'entrée qui représente un fichier de la base :
\begin{lstlisting}
typedef struct EntreeTable{
  char * nom_fichier; // Nom du fichier 
  long taille;        // Taille du fichier
  char* clef          // Clef MD5 du fichier 
  char * ip;          // Adresse IP du proprietaire 
  int port;           // Port de telechargement
}EntreeTable;
\end{lstlisting}

Bien évidemment, Le TAD \texttt{TableEntrée} est accompagnée d'une série de primitives qui facilitent l'interaction 
et l'interrogation de la base de données pour le thread effectuant le traitement de la requête du client.

\subsection*{Le Parseur de requêtes}
Notre démarche a suivi 2 phases :\\
\textbf{phase 1: L'utilisation d'une grammaire}\\
Au vu de la simplicité du protocole de communication proposé, il nous est paru évident d'exploiter nos connaissances acquises 
en cours de Compilation et générer un front-end basée sur cette grammaire et s'occupera de parser les requêtes
et d'effectuer l'analyse syntaxique pour vérifier son intégrité. Nous avons formé une grammaire en utilisant un logiciel open-source
pour générer le parser. Mais étant donné que \og{}TCP est orienté flux : une lecture sur la socket ne retourne pas forcément un message 
complet\fg{}, le traitement doit se faire au fur et à mesure de la réception.\\
\textbf{phase 2: Implémentation d'un automate}\\
Nous avons donc implémenté un parseur \og{}à la main\fg{} qui analyse chaque fragment de message reçus et passe à un état ou traitement
en fonction de la nature requête. Cette approche pourra être sujet de modification vu le caractère \og{}indigeste\fg{} du code
qui s'apprête difficilement à la modularité.


\section*{Architecture Globale}
Dans cette partie, nous présentons des ilustrations graphique de l'architecture respectivement celle du tracker tracker \ref{Tracker} et 
celle du pair \ref{Pair}\\
\begin{figure}[ht]
\centering
\includegraphics[scale=0.4]{tracker}
\caption{Architecture du Tracker}
\label{Tracker}
\end{figure}

\begin{figure}[ht]
\centering
\includegraphics[scale=0.4]{client_graph}
\caption{Architecture du pair}
\label{Pair}
\end{figure}

\section*{Les commandes}
Lorsque un pair se connecte, lorsque la connexion est acceptée, la connexion est déléguée à un thread. Cette connexion perdure tant que le client
envoie des requêtes ou sinon tant qu'il n'y a pas de \textit{TimeOut} (définie sous forme de constante dans le fichier de configuration)  
\subsection*{La commande \texttt{announce}}
Il s'agit de la première requête instiguée par le pair dès sa connexion afin de signaler sa présence au \texttt{Tracker} ainsi que la liste des 
pièces possédées, le pair forme une requête \texttt{announce} selon le format :
\begin{lstlisting}
announce listen $Port seed [$name1 $size1 $PSize1 $Key1 ...] leech [...]
\end{lstlisting}
Cette requête est envoyé au tracker qui la receptionne et effectuant le traitement suivant :
\begin{itemize}
\item On interroge la base de données du Tracker pour vérifier si le pair n'est pas enregistré dans la base de données
\item Si le cas échéant, on insére les différents élements relatifs au pair dans la base de données
\item Sinon, si une information relative au pair a été modifiée depuis la dernière consultation (exemple : statut du pair qui passe de \verb+LEECHER+ en \verb+SEEDER+), une mise à jour est effectuée sinon rien n'est fait ce qui garantit l'intégrité des données.
\item Même traitement est effectué pour les fichiers 
\end{itemize}
Dans le cas où le traitement s'est bien effectué, le tracker renvoie un message de confirmation : \texttt{ok}.
Tout ceci survient bien entendu automatiquement.


\subsection*{La commande look/list}
Un pair peut à tout moment demander au Tracker la liste des fichiers vérifiant un certain nombre de critères. Pour cela, il suffit d'envoyer une 
requête \texttt{look}. Le tracker peut jusqu'à présent gérer 2 critères de selection au niveau de la base de données (nom du fichier et sa taille).
Les critères de selection sont convertis en requêtes SQL au niveau du tracker opérant sur le nom et la taille des fichiers et renvoie la réponse
selon le format de la requête \texttt{list} contenant les informations sur les fichiers correspondant à la requête émise notamment la clé qui permet
de l'identifier de manière unique (afin de le récupérer plus tard via la requête \texttt{getfile}).

\begin{lstlisting}
 < look [$Criterion1 $Criterion2 ...]
 > list [$Filename1 $Length1 $PieceSize1 $Key1 ...]
\end{lstlisting}


\subsection*{La commande getfile/peers}
Ayant récupéré la liste des fichiers correspondants, Le pair peut, s'il le désire, demander le téléchargement d'un fichier au Tracker qui se traduit par la formulation d'une requête \texttt{getfile}. De la même manière que pour la requête \texttt{look}, le tracker receptionne la demande et interroge
la base pour fournir la liste des pairs possédant tout ou une partie du fichier en question (à partir de sa clé) ainsi que les informations pour se connecter à ses différents pairs.
dispose maintenant de la liste des fichiers enregistrée dans la base de données du Tracker.
\begin{lstlisting}
 < getfile $Key
 > peers $Key [$IP1:$Port1 $IP2:$Port2 ]
\end{lstlisting}
Le pair récupère ainsi toutes les informations nécéssaires pour télécharger les fichiers qu'il souhaite acquérir. 

\subsection*{La commande interested/have}
Cette partie s'effectue automatique après une requête \texttt{getfile}.
Après avoir obtenu la liste des pairs auxquels se connecter, Les informations sur les fichiers et les pairs qui hébergent les parties du fichier
sont stockés au sein d'une base de données du côté client. Un thread est créé pour s'occuper de former la requête de demande de téléchargement (\texttt{interested} et d'envoyer cette requête aux pairs ayant une partie du fichier. Le thread d'écoute (qui tourne toujours) du pair \verb+seeder+
intercepte la requête et délégue à un nouveau de thread le soin de former la requête \texttt{have} qui fournit le buffermap (stocké dans la base du pair) concernant les parties du fichier qu’il a en leur possession \verb+seeder+.
\begin{lstlisting}
 < interested $Key
 > have $Key $BufferMap
\end{lstlisting}
    
\subsection*{La commande getpieces/data}
Le pair demandeur possède les parties des fichiers possédées par chaque pair \verb+seeder+. Pour recevoir ces données, le pair forme une requête
pour demander à chaque pair une liste de pièces du fichier à télécharger (liste précisée par l'algorithme de téléchargement ci-dessous). La partie centrale de cette partie est d'implémenter un algorithme qui permettent de télécharger les différentes pièces du fichier (que l'on a supposé complet) à télécharger.\\
Une fois les pièces précisées pour chaque pair, un thread s'occupe de former la requête \texttt{getpieces} pour chaque
pair \verb+seeder+. Ce dernier intercepte cette requête via le thread d'écoute et délégue à un nouveau thread le traitement de cette requête. Le thread s'occupe d'abord de récupérer les pièces binaires du fichier formulées dans la requête
en utilisant les primitives du module \textit{Partition} et s'occuper de construire la requête \texttt{data} et se charge de la transmettre au pair demandeur. Ce dernier receptionne la requête, et avec les mêmes primitives, crée et reconstitue totalement ou partiellement le fichier en question.

\subsubsection*{Algorithme de téléchargement}
Afin de sélectionner d'une part le pair chez qui réaliser le
téléchargement, et d'autre part les pièces à télécharger chez
celui-ci, if faut définir un algorithme. Dans l'idéal, ce
dernier optimise le choix afin de maximiser les
téléchargements en parallèles.

Notre algorithme est plus naïf : il sélectionne le premier
pair disponible et les première pièces disponibles chez celui.
Cet algorithme risque de favoriser certains pairs, mais est néanmoins capable de télécharger toutes les pièces. 

\subsection*{Gestion du update au niveau du tracker}
Pour la gestion de la commande \verb+update+, nous avons ajouté un champ \verb+Timestamp+ dans la table \verb+Peer+. Ce champ est initialisé lors de la construction d'un peer et est mis à jour à chaque réception par le tracker de la requête update. Ainsi, grâce à l'ajout de ce champ, nous avons pu mettre en place une mise à jour de notre base de données. Nous avons décidé de supprimer tout peer qui n'effectue aucune requête \verb+update+ durant plus de 3 minutes. Pour cela le thread \verb+UpdateThread+ a été créé. Celui-ci, en évaluant la différence entre le Timestamp courant et les Timestamps de chaque peer, détermine l'ensemble des peer à supprimer de la base. 
Lors de la réception de la requête \verb+update+, en plus de mettre à jour le champ Timestamp, tous les champs du peers nécessitant une modification defini à travers la requête update le sont.



\section*{Piste d'amélioration du projet}
\textbf{Utilisation de }\texttt{hton} : 
En vue de garantie, une application réseau multi-plateformes, une amélioration serait d'envisager
l'utilisation de la fonction \texttt{hton} pour les données binaires pour les formater selon le format réseau (généricité).\\

\textbf{Suppression des fichiers inexistants} : 
Autrement dit, effectuer une mise à jour de la base de données pour éliminer les fichiers éventuellement supprimés. Nous avons entamé une réflexion la-dessus et nous sommes apercus que des fichiers d'un pair déconnecté peuvent être dans le réseau, ce qui impose concretement de vérifier les fichiers possédés par tous les pairs pour en supprimer les obsolètes.\\


%\textbf{Performance, stabilité et fuites mémoires} : 
 
\section*{Conclusion}
En conclusion, ceci fut un projet très enrichissant puisqu’il s’agit d'une première ébauche pour 
grand nombre de nous dans un projet concret qui réunit plusieurs compétences notamment la programmation réseau,
la programmation système et des aspects de génie logiciel.
Le vrai atout de ce projet se caractérise surtout par le fait que nous devions mobiliser ces divers compétences étudiées en cours, souvent théoriques, pour aboutir à notre application. C’était donc un projet très
intéressant qui aurait mérité d’être approfondi.\\
En définitive, ce fut un projet très intéressant et formateur pour tous les membres de
l’équipe.


\end{document}
